﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Json;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Text;
using System.ComponentModel;

namespace OrzLib
{
    public class DataEntityDataDirectoryNotFoundException : DirectoryNotFoundException { }
    public class DataEntityNotFoundException : IOException { }

    [Serializable]
    public class DataEntity<T> where T : new()
    {
        private bool _modified = false;
        public bool Modified
        {
            get { return _modified; }
            set { _modified = value; }
        }

        //Default Encoding is XML
        private static int _xml0Base64Str1Bytes2WCF3 = 0;
        //Set it to false will increase speed but large storage, set it to true will slow down speed but optimized storage
        private static bool _saveOptimizationEnable = true;

        public static bool SaveOptimizationEnable
        {
            get { return _saveOptimizationEnable; }
            set { _saveOptimizationEnable = value; }
        }

        private static DirectoryInfo _defaultDirectory = new DirectoryInfo(Path.Combine("DataEntity", typeof(T).Name));

        public string UniqueId { get; set; }

        public static DirectoryInfo DefaultDirectory
        {
            get
            {
                return _defaultDirectory;
            }
            set { _defaultDirectory = value; }
        }

        public static int Xml0Base64Str1Bytes2Wcf3
        {
            get { return _xml0Base64Str1Bytes2WCF3; }
            set { _xml0Base64Str1Bytes2WCF3 = value; }
        }

        public FileInfo GetFileId()
        {
            return new FileInfo(Path.Combine(DefaultDirectory.FullName, UniqueId));
        }

        public static FileInfo GetFileId(string uniqueId)
        {
            return new FileInfo(Path.Combine(DefaultDirectory.FullName, uniqueId));
        }

        public DataEntity()
        {
            UniqueId = Guid.NewGuid().ToString() + "." + this.GetType().Name;
        }

        public void Delete()
        {
            var fileId = GetFileId();
            fileId.Delete();
        }

        public bool Exists()
        {
            var fileId = GetFileId();
            return fileId.Exists;
        }

        public void Update()
        {
            try
            {
                UpdateBreathFirst(this);
            }
            catch (IOException)
            {
                throw new DataEntityNotFoundException();
            }
        }

        public void Save(bool checkModified = false)
        {
            SaveRecursive(this, checkModified);
            if (SaveOptimizationEnable) Update();
        }

        public void SaveIfModified()
        {
            SaveRecursive(this, true);
            if (SaveOptimizationEnable) Update();
        }

        public static void Delete(string uniqueId)
        {
            var fileId = GetFileId(uniqueId);
            fileId.Delete();
        }

        public static bool Exists(Guid uniqueId)
        {
            var fileId = new FileInfo(Path.Combine(DefaultDirectory.FullName, uniqueId.ToString()));
            return fileId.Exists;
        }

        public static T Update(string uniqueId)
        {
            T t = new T();
            t.GetType().GetProperty("UniqueId").SetValue(t, uniqueId, null);
            UpdateBreathFirst(t);
            return t;
        }

        public static void Save(DataEntity<T> t, bool checkModified = false)
        {
            SaveRecursive(t, checkModified);
        }

        public static void SaveIfModified(DataEntity<T> t)
        {
            SaveRecursive(t, true);
        }

        public static List<T> List()
        {
            var resultList = new List<T>();
            try
            {
                var dir = DefaultDirectory;
                foreach (var fileId in dir.GetFiles())
                {
                    T t = new T();
                    if (fileId.Name.Contains(t.GetType().Name))
                    {
                        t.GetType().GetProperty("UniqueId").SetValue(t, fileId.Name, null);
                        t.GetType().GetMethod("Update", new Type[] { }).Invoke(t, null);
                        resultList.Add(t);
                    }
                }
            }
            catch (DirectoryNotFoundException) { }
            catch (IndexOutOfRangeException) { }
            return resultList;

        }

        public static List<O> List<O>()
        {
            var resultList = new List<O>();
            try
            {
                var dir = DefaultDirectory;
                foreach (var fileId in dir.GetFiles())
                {
                    object t = Activator.CreateInstance(typeof(O));
                    if (fileId.Name.Contains(t.GetType().Name))
                    {
                        t.GetType().GetProperty("UniqueId").SetValue(t, fileId.Name, null);
                        t.GetType().GetMethod("Update", new Type[] { }).Invoke(t, null);
                        resultList.Add((O)t);
                    }
                }
            }
            catch (DirectoryNotFoundException) { }
            catch (IndexOutOfRangeException) { }
            return resultList;

        }

        public static List<string> ListUniqueIds()
        {
            var resultList = new List<string>();
            try
            {
                var dir = DefaultDirectory;
                foreach (var fileId in dir.GetFiles())
                {
                    resultList.Add(fileId.Name);
                }

            }
            catch (DirectoryNotFoundException) { }
            catch (IndexOutOfRangeException) { }
            return resultList;

        }

        public static IEnumerable<T> Enumerable()
        {
            if (!DefaultDirectory.Exists) yield break;

            var files = DefaultDirectory.GetFiles();
            if (files.Length < 0)
            {
                yield break;
            }
            for (int i = 0; i < files.Length; i++)
            {
                var fileId = files[i];
                T t = new T();
                if (fileId.Name.Contains(t.GetType().Name))
                {
                    t.GetType().GetProperty("UniqueId").SetValue(t, fileId.Name, null);
                    t.GetType().GetMethod("Update", new Type[] { }).Invoke(t, null);
                    yield return t;
                }
            }
        }

        public static IEnumerable<TType> Enumerable<TType>()
        {
            if (!DefaultDirectory.Exists) yield break;

            var files = DefaultDirectory.GetFiles();
            if (files.Length < 0)
            {
                yield break;
            }
            for (int i = 0; i < files.Length; i++)
            {
                var fileId = files[i];
                object t = Activator.CreateInstance(typeof(TType));
                if (fileId.Name.Contains(t.GetType().Name))
                {
                    t.GetType().GetProperty("UniqueId").SetValue(t, fileId.Name, null);
                    t.GetType().GetMethod("Update", new Type[] { }).Invoke(t, null);
                    yield return (TType)t;
                }
            }
        }

        public static void CleanDirectory()
        {
            var dir = DefaultDirectory;
            foreach (var fileId in dir.GetFiles())
            {
                fileId.Delete();
            }
        }

        protected static void UpdateBreathFirst(object obj)
        {
            var val = obj.GetType().GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(obj, null);
            ReflectionValueAssign(obj, val);
            var rootQueue = new Queue<object>();
            EnqueueChildren(obj, rootQueue);
            while (rootQueue.Count > 0)
            {
                var next = rootQueue.Dequeue();
                EnqueueChildren(next, rootQueue);

                var newObj = next.GetType().GetMethod("Deserialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(next, null);
                ReflectionValueAssign(next, newObj);
            }
        }

        protected static void EnqueueChildren(object obj, Queue<object> rootQueue)
        {
            if (ReflectionBaseTypeCompare(obj.GetType(), typeof(DataEntity<>)))
            {
                foreach (var propertyInfo in obj.GetType().GetProperties())
                {
                    if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(DataEntity<>)))
                    {
                        var item = propertyInfo.GetValue(obj, null);
                        if (item != null)
                        {
                            rootQueue.Enqueue(item);
                        }
                    }
                    else if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(List<>)))
                    {
                        var list = (IList)propertyInfo.GetValue(obj, null);
                        if (list != null)
                        {
                            foreach (object item in list)
                            {
                                rootQueue.Enqueue(item);
                            }

                        }

                    }
                }
            }
        }

        protected static void SaveRecursive(object obj, bool checkModified)
        {
            //if base type is DataEntity, then we treat it as entity
            if (ReflectionBaseTypeCompare(obj.GetType(), typeof(DataEntity<>)))
            {
                foreach (var propertyInfo in obj.GetType().GetProperties())
                {
                    if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(DataEntity<>)))
                    {
                        var item = propertyInfo.GetValue(obj, null);
                        if (item != null)
                        {
                            SaveRecursive(item, checkModified);
                            // Clean Entity content at current depth  
                            if (SaveOptimizationEnable) CleanEntityContent(item);
                        }
                    }
                    else if (ReflectionBaseTypeCompare(propertyInfo.PropertyType, typeof(List<>)))
                    {
                        var list = (IList)propertyInfo.GetValue(obj, null);
                        if (list != null)
                        {
                            foreach (object item in list)
                            {
                                SaveRecursive(item, checkModified);
                                // Clean Entity content at current depth  
                                if (SaveOptimizationEnable) CleanEntityContent(item);

                            }
                        }
                    }
                }

                if (checkModified)
                {
                    var modified = (bool)obj.GetType().GetProperty("Modified").GetValue(obj, null);
                    if (modified)
                        obj.GetType().GetMethod("Serialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(obj, null);
                }
                else
                {
                    obj.GetType().GetMethod("Serialize", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic).Invoke(obj, null);
                }
            }

        }

        protected object Deserialize()
        {
            var fileId = GetFileId();
            return DeserializeFromFile(this, fileId, Xml0Base64Str1Bytes2Wcf3);
        }

        protected void Serialize()
        {
            var fileId = GetFileId();
            SerializeToFile(this, fileId, Xml0Base64Str1Bytes2Wcf3);
        }

        protected static void SerializeToFile(object obj, FileInfo fileId, int xml0_Base64Str1_Bytes2_WCF3)
        {
            CreateDirectoryIfNotExists(fileId.Directory);
            if (0 == xml0_Base64Str1_Bytes2_WCF3)
                File.WriteAllText(fileId.FullName, SerializationUtility.ToXml(obj));
            else if (1 == xml0_Base64Str1_Bytes2_WCF3)
                File.WriteAllText(fileId.FullName, SerializationUtility.ToBas64String(obj));
            else if (2 == xml0_Base64Str1_Bytes2_WCF3)
                File.WriteAllBytes(fileId.FullName, SerializationUtility.ToBytes(obj));
            else
                File.WriteAllText(fileId.FullName, SerializationUtility.ToWcfxml(obj));
        }

        protected static object DeserializeFromFile(object obj, FileInfo fileId, int xml0_Base64Str1_Bytes2_WCF3)
        {
            object newObj = null;
            if (0 == xml0_Base64Str1_Bytes2_WCF3)
                newObj = SerializationUtility.ToObject(File.ReadAllText(fileId.FullName), obj.GetType());
            else if (1 == xml0_Base64Str1_Bytes2_WCF3)
                newObj = SerializationUtility.ToObject(File.ReadAllText(fileId.FullName));
            else if (2 == xml0_Base64Str1_Bytes2_WCF3)
                newObj = SerializationUtility.ToObject(File.ReadAllBytes(fileId.FullName));
            else
                newObj = SerializationUtility.WcfToObject(File.ReadAllText(fileId.FullName), obj.GetType());

            return newObj;
        }

        private static void CleanEntityContent(object item)
        {
            object emptyItem = Activator.CreateInstance(item.GetType());
            object existingId = item.GetType().GetProperty("UniqueId").GetValue(item, null);
            emptyItem.GetType().GetProperty("UniqueId").SetValue(emptyItem, existingId, null);
            ReflectionValueAssign(item, emptyItem);
        }

        private static void ReflectionValueAssign(object a, object b)
        {
            if (ReflectionBaseTypeCompare(a.GetType(), b.GetType()))
            {
                foreach (var p in a.GetType().GetProperties())
                {
                    foreach (var p2 in b.GetType().GetProperties())
                    {
                        if (p.Name == p2.Name)
                        {
                            p.SetValue(a, p2.GetValue(b, null), null);
                        }
                    }
                }
            }
        }

        private static bool ReflectionBaseTypeCompare(Type t, Type t2)
        {
            if (t.FullName.StartsWith(t2.FullName)) return true;
            if (t == typeof(object)) return false;
            return ReflectionBaseTypeCompare(t.BaseType, t2);
        }

        private static void CreateDirectoryIfNotExists(DirectoryInfo dirInfo)
        {
            if (dirInfo == null) return;
            if (dirInfo.Parent != null) CreateDirectoryIfNotExists(dirInfo.Parent);
            if (!dirInfo.Exists) dirInfo.Create();
        }

    }

    /// <summary>
    /// Utility class for serializing and deserializing objects
    /// </summary>
    internal static class SerializationUtility
    {

        public static string ToJson(object value)
        {
            if (value == null)
            {
                return null;
            }

            var serializer = new DataContractJsonSerializer(value.GetType());
            using (var dataInMemory = new MemoryStream())
            {
                serializer.WriteObject(dataInMemory, value);
                return Encoding.Default.GetString(dataInMemory.ToArray());
            }

        }

        public static string ToWcfxml(object value)
        {
            if (value == null)
            {
                return null;
            }

            using (var dataInMemory = new MemoryStream())
            {
                new DataContractSerializer(value.GetType()).WriteObject(dataInMemory, value);
                return Encoding.Default.GetString(dataInMemory.ToArray());
            }
        }

        public static string ToXml(object value)
        {
            if (value == null)
            {
                return null;
            }
            using (var inMemoryData = new StringWriter())
            {
                new XmlSerializer(value.GetType()).Serialize(inMemoryData, value);
                return inMemoryData.ToString();
            }
        }

        public static byte[] ToBytes(object value)
        {
            if (value == null)
            {
                return null;
            }

            byte[] inMemoryBytes;
            using (var inMemoryData = new MemoryStream())
            {
                new BinaryFormatter().Serialize(inMemoryData, value);
                inMemoryBytes = inMemoryData.ToArray();
            }

            return inMemoryBytes;
        }

        public static string ToBas64String(object value)
        {
            return Convert.ToBase64String(ToBytes(value));
        }

        public static object ToObject(byte[] byteData)
        {
            if (byteData == null)
            {
                return null;
            }

            using (var dataInMemory = new MemoryStream(byteData))
            {
                return new BinaryFormatter().Deserialize(dataInMemory);
            }
        }

        public static object ToObject(string base64String)
        {
            return ToObject(Convert.FromBase64String(base64String));
        }

        public static object ToObject(string xml, Type t)
        {
            if (xml == null || t == null)
            {
                return null;
            }

            using (var dataInMemory = new StringReader(xml))
            {
                return new XmlSerializer(t).Deserialize(dataInMemory);

            }

        }

        public static object WcfToObject(string xml, Type t)
        {
            if (xml == null || t == null)
            {
                return null;
            }

            using (var dataInMemory = new MemoryStream(Encoding.Default.GetBytes(xml)))
            {
                return new DataContractSerializer(t).ReadObject(dataInMemory);
            }
        }

        public static object JsonToObject(string xml, Type t)
        {
            if (xml == null || t == null)
            {
                return null;
            }

            using (var dataInMemory = new MemoryStream(Encoding.Default.GetBytes(xml)))
            {
                return new DataContractJsonSerializer(t).ReadObject(dataInMemory);
            }
        }

    }
}